package de.dnb.basics.utils;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

import org.marc4j.MarcStreamReader;
import org.marc4j.MarcXmlReader;
import org.marc4j.marc.ControlField;
import org.marc4j.marc.DataField;
import org.marc4j.marc.Record;
import org.marc4j.marc.Subfield;
import org.marc4j.marc.VariableField;

import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.basics.applicationComponents.StringInputStream;
import de.dnb.basics.applicationComponents.StringUtils;

public final class MarcUtils {

    private MarcUtils() {
    }

    /**
     * Liest Record im xml-Format aus der Zwischenablage.
     * 
     * @return	record oder null.
     */
    public static Record readXMLfromClip() {
        String xml = StringUtils.readClipboard();
        if (xml == null)
            return null;
        if (!xml.startsWith("<?xml version")) {
            xml =
                "<?xml version=\"1.0\"?>"
                    + "<mx:collection xmlns:mx=\"http://www.loc.gov/MARC21/slim\">"
                    + xml + "</mx:collection>";
        }
        /*
         * .getBytes():
         * "This method always replaces malformed-input and
         * unmappable-character sequences with this charset's default
         * replacement byte array."
         */
        InputStream input =
            new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
        //		        new StringInputStream(xml);
        MarcXmlReader reader = new MarcXmlReader(input);
        Record record = reader.next();
        return record;
    }

    /**
     * Ersetzt '$i' durch ' |$|:'.
     * 
     * @param record	nicht null
     * @return			besser lesbar
     */
    public static String readableFormat(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String s = record.toString();
        s = s.replaceAll("\\$(.)", " |$1|:");
        return s;
    }

    /**
     * List einen Datensatz im Marc-Format von der Zwischenablage.
     * 
     * @return	Datensatz oder null
     */
    public static Record readfromClip() {
        String strg = StringUtils.readClipboard();
        if (strg == null)
            return null;
        StringInputStream inputStr = new StringInputStream(strg);
        MarcStreamReader reader = new MarcStreamReader(inputStr);
        Record record = reader.next();
        return record;
    }

    /**
     * 
     * @param record nicht null
     * @param tag		nicht null
     * @return
     */
    public static boolean containsField(final Record record, final String tag) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        RangeCheckUtils.assertReferenceParamNotNull("tag", tag);
        List<VariableField> fields = record.getVariableFields(tag);
        return !fields.isEmpty();
    }

    /**
     * Enthält das Datenfeld den code (Code ist unser Indikator)?
     * 
     * @param dataField	nicht null
     * @param code		beliebig
     * @return
     */
    public static boolean containsCode(
        final DataField dataField,
        final char code) {
        RangeCheckUtils.assertReferenceParamNotNull("dataField", dataField);
        return !dataField.getSubfields(code).isEmpty();
    }

    /**
     * Hilfsmethode mit varargs.
     * 
     * @param record nicht null
     * @param tags	nicht null
     * @return		alle variablen Felder
     */
    public static List<VariableField> getVariableFields(
        final Record record,
        final String... tags) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        RangeCheckUtils.assertReferenceParamNotNull("tags", tags);
        return record.getVariableFields(tags);
    }

    /**
     * 
     * @param record	nicht null
     * @param tags		nicht null
     * @return			Liste der Datenfelder (nicht null) oder wirft 
     * 					IllegalArgumentException, wenn
     * 					einer der tags zu keinem Datenfeld gehört
     */
    public static List<DataField> getDataFields(
        final Record record,
        final String... tags) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        RangeCheckUtils.assertReferenceParamNotNull("tags", tags);
        List<DataField> dataFields = new LinkedList<>();
        for (VariableField variableField : record.getVariableFields(tags)) {
            try {
                dataFields.add((DataField) variableField);
            } catch (ClassCastException e) {
                throw new IllegalArgumentException(variableField.getTag()
                    + " ist kein Datenfeld-Tag");
            }
        }
        return dataFields;
    }

    /**
     * Liefert das erste Datenfeld, dessen Tag mit '1' beginnt, also
     * das Feld, das den bevorzugten Namen enthält.
     * 
     * @param record 	nicht null
     * @return			Heading oder null
     */
    public static DataField getHeadingField(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> datafields = record.getDataFields();
        for (DataField dataField : datafields) {
            String tag = dataField.getTag();
            if (StringUtils.charAt(tag, 0) == '1')
                return dataField;
        }
        return null;
    }

    /**
     * Gibt die Felder, die Oberbegriffe enthalten.
     * 
     * @param record	nicht null
     * @return			nicht null
     */
    public static Collection<DataField> getOBBFields(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> obList = new LinkedList<>();
        List<DataField> datafields = record.getDataFields();
        for (DataField dataField : datafields) {
            String tag = dataField.getTag();
            if (StringUtils.charAt(tag, 0) == '5')
                obList.add(dataField);
        }
        return obList;
    }

    /**
     * Gibt die Quellen.
     * 
     * @param record	nicht null
     * @return			670-Felder, nicht null
     */
    public static Collection<DataField> getSourceFields(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> sources = new LinkedList<>();
        List<VariableField> sourcefields = record.getVariableFields("670");
        for (VariableField variableField : sourcefields) {
            sources.add((DataField) variableField);
        }
        return sources;
    }

    /**
     * Gibt die Erläuterungen zum Datensatz.
     * 
     * @param record	nicht null
     * @return			680-Felder, nicht null
     */
    public static Collection<DataField> getPublicGeneralNoteFields(
        final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> notes = new LinkedList<>();
        List<VariableField> notefields = record.getVariableFields("680");
        for (VariableField variableField : notefields) {
            notes.add((DataField) variableField);
        }
        return notes;
    }

    /**
     * 
     * Die id.
     * 
     * @param record	nicht null
     * @return			id
     */
    public static String getContolNumber(final Record record) {
        Objects.requireNonNull(record);
        return record.getControlNumber();
    }

    /**
     * 
     * Gibt das Feld fester Länge:<br>
     * Fourteen character positions (00-13) that contain positionally-defined 
     * data elements that provide coded information about the record as a 
     * whole or about data in field 153 (Classification Number). Each 
     * character position must contain either a defined code or a fill 
     * character ( | ). 
     * 
     * @param record    nicht null
     * @return          Feld 008
     */
    public static String getFixedLengthDataElements(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        ControlField fixedField = (ControlField) record.getVariableField("008");
        return fixedField.getData();
    }

    /**
     * Gibt die verlinkten Datensätze aus den Erläuterungen zum Datensatz, also
     * die $a-Unterfelder der 680- und 681-Felder.
     * 
     * @param record	nicht null
     * @return			$a der 680- und 681-Felder, nicht null
     */
    public static Collection<String> getHeadingFromPublicGeneralNoteFields(
        final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<String> links = new LinkedList<>();
        List<DataField> notefields = getDataFields(record, "680", "681");
        for (DataField dataField : notefields) {
            List<Subfield> dollarAs = (dataField).getSubfields('a');
            for (Subfield dollarA : dollarAs) {
                links.add(dollarA.getData());
            }
        }
        return links;
    }

    /**
     * Gibt die Verweisungsvermerke zum Datensatz.
     * 
     * @param record	nicht null
     * @return			681-Felder, nicht null
     */
    public static
        Collection<DataField>
        getTracingNoteFields(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> notes = new LinkedList<>();
        List<VariableField> notefields = record.getVariableFields("681");
        for (VariableField variableField : notefields) {
            notes.add((DataField) variableField);
        }
        return notes;
    }

    /**
     * Gibt die Synonyme.
     * 
     * @param record	nicht null
     * @return			Felder, deren Tag mit '4' beginnt, nicht null
     */
    public static Collection<DataField> getSynonymFields(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> obList = new LinkedList<>();
        List<DataField> datafields = record.getDataFields();
        for (DataField dataField : datafields) {
            String tag = dataField.getTag();
            if (StringUtils.charAt(tag, 0) == '4')
                obList.add(dataField);
        }
        return obList;
    }

    public static void main(final String[] args) {

        Record record = readXMLfromClip();
        System.out.println(getFixedLengthDataElements(record));
    }

}
